<?php
// @formatter:off
/**
 * @file WCParser.php
 *
 * @author Alejandro Dario Simi
 * @date $Date: 2013-07-11 21:02:48 +0000 (Thu, 11 Jul 2013) $
 *
 * $Id: WCParser.php 95 2013-07-11 21:02:48Z daemonraco@gmail.com $
 * $URL: http://wcomix.googlecode.com/svn/tags/wcomix-1.0.0.2/includes/parser/WCParser.php $
 */
// @formatter:on

/**
 * Including all basic behaviors.
 * @{
 */
require_once dirname(__FILE__)."/WCParserNode.php";
require_once dirname(__FILE__)."/WCParserBehaviorIGNORE.php";
require_once dirname(__FILE__)."/WCParserBehaviorVAR.php";
require_once dirname(__FILE__)."/WCParserBehaviorIF.php";
require_once dirname(__FILE__)."/WCParserBehaviorISSET.php";
require_once dirname(__FILE__)."/WCParserBehaviorINCLUDE.php";
require_once dirname(__FILE__)."/WCParserBehaviorOVER.php";
/** @} */

/**
 * @class WCParser This is a main class that provides a text parsing system based
 * on tags.
 * This class and those that compose it work on the idea of the next pseudo-code:
 * @code
 * 	Some text <:FUNC:parametres:/>contents<::/> and some more text.
 * @endcode
 */
class WCParser {
	/**
	 * @var string This is the indicator for any tag that is been open.
	 */
	public static $TagOpen = "<:";
	/**
	 * @var string This is the indicator for any tag that is been closed.
	 */
	public static $TagClose = ":/>";
	/**
	 * @var string This is the separator of parameter inside a tag
	 * specification.
	 */
	public static $ParamSeparator = ":";
	/**
	 * @var string This is the type of warnings used when a variable is
	 * requested and it doesn't exists.
	 */
	const WarningUNSET_VARIABLE = "UNSET_VARIABLE";
	/**
	 * @var string This is a sort of cache of the last code generated by the
	 * method parse.
	 */
	protected $_code = false;
	/**
	 * @var int This is how deep a node can be nested.
	 */
	protected $_maxDepth = 10;
	/**
	 * @var WCParserNode[] This is the list of every node held by this class.
	 * This is a array representation of nodes.
	 */
	protected $_nodes = null;
	/**
	 * @var boolean This flag indicates that the last loading operation was
	 * successful.
	 */
	protected $_ok = false;
	/**
	 * @var string Full system path for the last loaded template.
	 */
	protected $_path = false;
	/**
	 * @var WCParserNode This is the root for all node held by this class.
	 * This is a tree representation of nodes.
	 */
	protected $_root = null;
	/**
	 * @var boolean This flag indicated if the summary of each parsing is
	 * enabled or not.
	 */
	protected $_showSummary = true;
	/**
	 * @var boolean This flag indicates if warning are set to be shown or not.
	 */
	protected $_showWarnings = true;
	/**
	 * @var string Current templates full system path.
	 */
	protected $_templatesRoot = "";
	/**
	 * @var string[] List of variables and their values to be used when
	 * parsing text.
	 */
	protected $_variables = array();
	/**
	 * @var array[string] List of warning types an their reported warnings.
	 */
	protected $_warnings = array();
	/**
	 * Class constructor.
	 */
	public function __construct() {
		//
		// Setting a type of warnings for variables that hasn't been when
		// required.
		$this->setWarning(WCParser::WarningUNSET_VARIABLE, false, false, "Requested variables that have not been set");
	}
	//
	// Public pseudo-methods.
	/**
	 * This method allows to get the value of certain variable previously set.
	 *
	 * @var string $varname Name of a variable to get.
	 * @return mixed Returns it's value unless it's not set, in that case, it
	 * return null, adds the error to the warnings list and, if it's enabled,
	 * triggers a warning message.
	 */
	public function __get($varname) {
		//
		// Checking if the variable has been set before.
		if($this->has($varname)) {
			//
			// Returning the variables value.
			return $this->_variables[$varname];
		} else {
			//
			// Adding a new warning
			$this->setWarning(WCParser::WarningUNSET_VARIABLE, "Variable '{$varname}' is not set", false, false, __CLASS__);
			//
			// Checking if if warning triggers are activated.
			if($this->_showWarnings) {
				//
				// Triggering a warning message.
				trigger_error(__CLASS__."::".__FUNCTION__."(): ".__CLASS__." has no variable '{$varname}' set", E_USER_WARNING);
			}
			//
			// Returning null because the variable is not present.
			return null;
		}
	}
	/**
	 * This method sets a variable to use when parsing text.
	 *
	 * @param string $varname Name for the variable to set.
	 * @param mixed $value Value to associate with the variable.
	 * @return mixed Returns the value.
	 */
	public function __set($varname, $value) {
		$this->_variables[$varname] = $value;
		return $value;
	}
	//
	// Public methods.
	/**
	 * This is a more polite way to set a variable, but in the end, it's
	 * similar to __set().
	 *
	 * @param string $varname Name for the variable to set.
	 * @param string $value Value to associate with the variable.
	 * @return mixed Returns the value.
	 */
	public function assign($varname, &$value) {
		$this->_variables[$varname] = $value;
		return $value;
	}
	/**
	 * Checks if a variable has been set.
	 *
	 * @param string $varname Name of the variable to be checked.
	 * @return boolean Returns true if the variable has been set.
	 */
	public function has($varname) {
		return isset($this->_variables[$varname]);
	}
	/**
	 * This method allows to load the parser with the text to be parsed. Such
	 * file is also called template.
	 *
	 * @param string $file Template path relative to $this->_templatesRoot. It
	 * always must start with '/' (slash).
	 * @param string $full If this parameter is true, $this->_templatesRoot is
	 * ignored and $file is considered as full path.
	 * @return boolean Returns true when it loaded without problems.
	 */
	public function load($file, $full = false) {
		//
		// Building file's full system path.
		$this->_path = WCSanitizer::SystemPath(($full ? "" : $this->_templatesRoot).$file);
		//
		// If it's not possible to read a template file, WCParser triggers
		// an abort signal.
		if(!is_readable($this->_path)) {
			trigger_error(__CLASS__."::".__FUNCTION__."(): Unable to read file '{$this->_path}'", E_USER_ERROR);
		}
		//
		// Cleaning previous parsing data.
		// @{
		if($this->_root != null) {
			$this->_root = null;
		}
		$this->_nodes = array();
		$this->_code = false;
		// @}
		//
		// Loading file contents.
		$content = file_get_contents($this->_path);
		//
		// Calculating the length of opening and closing tags.
		$openLen = strlen(WCParser::$TagOpen);
		$closeLen = strlen(WCParser::$TagClose);
		//
		// Setting the initial node id.
		$id = 0;
		//
		// Calculating the length of the text to analyze.
		$len = strlen($content);
		//
		// Creating root node.
		$this->_root = new WCParserNode($id++, $this);
		$currId = $id++;
		//
		// This stack tracks parent nodes when nesting.
		$parents = array();
		//
		// Analyzes all the contents until there's no more.
		while($content) {
			//
			// If the current node is not yet set in the nodes' list
			// it is created and added.
			if(!isset($this->_nodes[$currId])) {
				//
				// Creating and adding the new node the lt.
				$this->_nodes[$currId] = new WCParserNode($currId, $this);
				//
				// Adding it as a child of the root node.
				$this->_root->addChild($this->_nodes[$currId]);
			}
			//
			// Searching for a tag opening mark.
			$openPos = strpos($content, WCParser::$TagOpen);
			//
			// Checking if there are some text before the opening tag.
			if($openPos > 0) {
				//
				// Lasting text before the tag is added to the
				// current node.
				$aux = substr($content, 0, $openPos);
				$this->_nodes[$currId]->addChild($aux);
				//
				// Current contents are reduced.
				$content = substr($content, $openPos);
			} elseif($openPos === false) {
				//
				// If the search reaches the end of all contents,
				// it adds the rest of the text to the current
				// node.
				$this->_nodes[$currId]->addChild($content);
				//
				// Current contents are reduced to nothing.
				$content = "";
			} else {
				// If the current content starts with an opening
				// tag mark, it analyzes it
				//
				// Getting the tag closing mark.
				$closePos = strpos($content, WCParser::$TagClose);
				//
				// If the tag closing mark isn't found could cause
				// errors, therefore the process aborts.
				if($closePos === false) {
					trigger_error(__CLASS__."::".__FUNCTION__."(): Tag closing mark '{WCParser::$TagClose}' hasn't been found in '{$content}'", E_USER_ERROR);
				}
				//
				// Getting the command to be performed.
				$command = substr($content, $openLen, $closePos - $closeLen + 1);
				//
				// Current content is reduced.
				$content = substr($content, $closePos + $closeLen);
				//
				// if $command is not empty, it means it's not a
				// closing tag.
				if($command) {
					//
					// Expanding the command.
					$command = explode(WCParser::$ParamSeparator, $command);
					//
					// Creating a node to manage this
					// subsection.
					$auxNode = new WCParserNode($id, $this);
					//
					// Setting the current node as its parent.
					$auxNode->parentNode($this->_nodes[$currId]);
					//
					// The first element of the expanded
					// command is the function name.
					$auxNode->func(array_shift($command));
					//
					// The rest is the parameters to be used
					// by the function.
					$auxNode->params($command);
					//
					// Adding the current node id to the
					// parents stack.
					array_push($parents, $currId);
					//
					// From now on, the current id is the new
					// one.
					$currId = $id;
					//
					// Adding the new node to the list.
					$this->_nodes[$id++] = $auxNode;
				} else {
					//
					// This is a closing tag.
					//
					// Jumping back the current id to the
					// parent based on the stack.
					$auxId = $currId;
					$currId = array_pop($parents);
					//
					// Until now, the last created node is an
					// orphan one and since this is a closing
					// for its specification, it is added to
					// the last parent.
					if(isset($this->_nodes[$currId]) && isset($this->_nodes[$auxId])) {
						$this->_nodes[$currId]->addChild($this->_nodes[$auxId]);
					} else {
						// If any of he nodes is broken
						// means something went wrong and
						// the process must abort.
						if(!isset($this->_nodes[$currId])) {
							trigger_error(__CLASS__."::".__FUNCTION__."(): Current node '{$currId}' is not set", E_USER_ERROR);
						} elseif(!isset($this->_nodes[$auxId])) {
							trigger_error(__CLASS__."::".__FUNCTION__."(): Node '{$auxId}' is not set", E_USER_ERROR);
						}
					}
				}
			}
		}
		//
		// Checking if every node is OK.
		$this->_ok = true;
		foreach($this->_nodes as $id => $node) {
			if(!($this->_ok = $node->isOk())) {
				break;
			}
		}
		//
		// Returning the loading status.
		return $this->_ok;
	}
	/**
	 * This method allows to set many variables in one call, instead of
	 * assigning values for each variable.
	 *
	 * @param mixed[string] $values Associative array containing each variable
	 * to be assigned, where keys are names and values are exactly that.
	 * @param boolean $replace When this variable is true, the current list of
	 * variables is replaces by $values.
	 */
	public function massiveAssign($values = array(), $replace = false) {
		//
		// Replacing means removing current values.
		if($replace) {
			$this->_variables = array();
		}
		//
		// Merging current values with the new ones. New values are more
		// important.
		$this->_variables = array_merge($this->_variables, $values);
	}
	/**
	 * Returns the value of how deep a node can be nested.
	 *
	 * @return int Nesting limit.
	 */
	public function maxDepth() {
		return $this->_maxDepth;
	}
	/**
	 * Sets the object to avoid prompting a summary at the end when a template
	 * is parsed.
	 *
	 * @param boolean $value When true disables the summary.
	 */
	public function noSummary($value = true) {
		$this->_showSummary = !$value;
	}
	/**
	 * Sets the object to avoid prompting warnings.
	 *
	 * @param boolean $value When true disables warnings.
	 */
	public function noWarnings($value = true) {
		$this->_showWarnings = !$value;
	}
	/**
	 * Current status of the object.
	 *
	 * @return boolean Returns current object's status.
	 */
	public function ok() {
		return $this->_ok;
	}
	/**
	 * This method executes all nodes and obtain from them a cleared text.
	 *
	 * @return string Returns the resulting text.
	 */
	public function parse() {
		//
		// Checking if there's a cached text.
		if($this->_code === false) {
			//
			// Checking if:
			//	- this parser has no issues,
			//	- there's a root node
			//	- and root node has no issues.
			if($this->ok() && $this->_root != null && $this->_root->isOk()) {
				//
				// Executing root node.
				$this->_code = $this->_root->parse();
			} elseif(!$this->ok()) {
				trigger_error(__CLASS__."::".__FUNCTION__."(): The parser is not right", E_USER_ERROR);
			} elseif($this->_root == null) {
				trigger_error(__CLASS__."::".__FUNCTION__."(): Root node is not set", E_USER_ERROR);
			} else {
				trigger_error(__CLASS__."::".__FUNCTION__."(): Root node is not right", E_USER_ERROR);
			}
			//
			// Adding a summary of the parsing process. Only when it's
			// not disabled.
			if($this->_showSummary) {
				$this->_code .= $this->summary();
			}
		}
		//
		// Returns the generated text.
		return $this->_code;
	}
	/**
	 * Force the object to re-parse.
	 *
	 * @return string Returns the resulting text.
	 */
	public function reparse() {
		$this->_code = false;
		return $this->parse();
	}
	/**
	 * Cleans variables and cached texts. Nonetheless, loaded templates and
	 * nodes won't be changed.
	 */
	public function reset() {
		$this->_code = false;
		$this->_variables = array();
	}
	/**
	 * Sets the value of how deep a node can be nested.
	 *
	 * @return int $depth Nesting limit to be set.
	 * @return int Nesting limit.
	 */
	public function setMaxDepth($depth) {
		$this->_maxDepth = $depth;
		return $this->maxDepth();
	}
	/**
	 * This method allows to set warning types and also also add actual
	 * warnings to each type list.
	 *
	 * @param string $type Type id.
	 * @param string $value value of the warning been added. When false,
	 * nothing is reported. This useful for type's settings.
	 * @param int $code Numerical code for the warning been added.
	 * @param string $description Brief description for the warning type. When
	 * false, no action is taken regarding warning type's descriptions.
	 * @param string $reporter Name of the entity that reported a warning.
	 */
	public function setWarning($type, $value = false, $code = false, $description = false, $reporter = false) {
		//
		// Checking if $type matches a warning type previously defined. If
		// not, it's defined.
		if(!isset($this->_warnings[$type])) {
			$this->_warnings[$type] = array(
				//
				// Warning type.
				"type" => $type,
				//
				// List of warnings reported for this type.
				"list" => array(),
				//
				// Description for the type of warnings.
				"description" => false
			);
		}
		//
		// If a description was set, warning type's description must
		// change.
		if($description !== false) {
			$this->_warnings[$type]["description"] = $description;
		}
		//
		// If a value was specified, it is added to the list for the
		// specified type.
		if($value !== false) {
			$this->_warnings[$type]["list"][] = ($code !== false ? "[{$code}] " : "").$value.($reporter !== false ? " [reported by {$reporter}] " : "");
		}
	}
	/**
	 * This method allows to know if the summary of each parsing is enabled or
	 * not.
	 *
	 * @return boolean Returns true when the summary is enabled.
	 */
	public function showSummary() {
		return $this->_showSummary;
	}
	/**
	 * This method allows to know if warning are set to be shown or not.
	 *
	 * @return boolean Returns true when warnings are enabled.
	 */
	public function showWarnings() {
		return $this->_showWarnings;
	}
	/**
	 * This methods allows to obtain the current template's root path set in
	 * the object.
	 *
	 * @param string $value When it is not false, it sets the root path. It
	 * must be an absolute path and a existing directory.
	 * @return string Returns the current root path.
	 */
	public function templatesRoot($value = false) {
		//
		// Checking if this method has been called to alter the current
		// setting.
		if($value !== false) {
			//
			// Clearing path.
			$value = WCSanitizer::SystemPath($value);
			//
			// Checking that it is an existing directory and a full
			// system path. Otherwise an error is triggered.
			if(is_dir($value) and WCSanitizer::IsAbsolutePath($value)) {
				$this->_templatesRoot = $value;
			} elseif(!is_dir($value)) {
				trigger_error(__CLASS__."::".__FUNCTION__."(): '{$value}' is not a directory", E_USER_ERROR);
			} else {
				trigger_error(__CLASS__."::".__FUNCTION__."(): '{$value}' is not an absolute path", E_USER_ERROR);
			}
		}
		//
		// Returning the current template's path.
		return $this->_templatesRoot;
	}
	/**
	 * Return the list of variables currently set in the object.
	 *
	 * @return mixed[string] List of variables.
	 */
	public function variables() {
		return $this->_variables;
	}
	//
	// Protected Methods.
	/**
	 * Generates a summary with all warnings and errors found on the parsing
	 * process.
	 *
	 * @return string Returns a string containing the summary in HTML format.
	 */
	protected function summary() {
		//
		// Opening a new DIV where the summary will be held. This kind of
		// DIVs share the same style called 'WCParserSummary'.
		$aux = "\n<div class=\"WCParserSummary\">\n";
		//
		// Summary title.
		$aux .= "\t<h4>Summary for '{$this->_path}':</h4>\n";
		//
		// Checking if there are warnings.
		if(count($this->_warnings)) {
			//
			// Analyzing each warning type.
			foreach($this->_warnings as $warning) {
				//
				// Checking if the current warning type has
				// reported warnings.
				if(count($warning["list"])) {
					//
					// Warning type title. List opening.
					$aux .= "\t<p><label".($warning["description"] ? " title=\"{$warning["description"]}\"" : "").">'{$warning["type"]}' warnings:</label><ul>\n";
					//
					// List of reported warnings.
					foreach($warning["list"] as $item) {
						$aux .= "\t\t<li>{$item}</li>\n";
					}
					//
					// Closing list.
					$aux .= "\t</ul></p>\n";
				}
			}
		}
		//
		// Closing warnings DIV.
		$aux .= "</div>\n";
		//
		// Returning built summary.
		return $aux;
	}
}
?>